/*
 * This file Copyright (C) 2008-2014 Mnemosyne LLC
 *
 * It may be used under the GNU GPL versions 2 or 3
 * or any future license endorsed by Mnemosyne LLC.
 *
 */

#include <errno.h>
#include <stdio.h>
#include <string.h>

#include <glib/gi18n.h>
#include <gtk/gtk.h>

#include <libtransmission/transmission.h>
#include <libtransmission/log.h>

#include "conf.h"
#include "hig.h"
#include "msgwin.h"
#include "tr-core.h"
#include "tr-prefs.h"
#include "util.h"

enum
{
    COL_SEQUENCE,
    COL_NAME,
    COL_MESSAGE,
    COL_TR_MSG,
    N_COLUMNS
};

struct MsgData
{
    TrCore* core;
    GtkTreeView* view;
    GtkListStore* store;
    GtkTreeModel* filter;
    GtkTreeModel* sort;
    tr_log_level maxLevel;
    gboolean isPaused;
    guint refresh_tag;
};

static struct tr_log_message* myTail = NULL;
static struct tr_log_message* myHead = NULL;

/****
*****
****/

/* is the user looking at the latest messages? */
static gboolean is_pinned_to_new(struct MsgData* data)
{
    gboolean pinned_to_new = FALSE;

    if (data->view == NULL)
    {
        pinned_to_new = TRUE;
    }
    else
    {
        GtkTreePath* last_visible;

        if (gtk_tree_view_get_visible_range(data->view, NULL, &last_visible))
        {
            GtkTreeIter iter;
            int const row_count = gtk_tree_model_iter_n_children(data->sort, NULL);

            if (gtk_tree_model_iter_nth_child(data->sort, &iter, NULL, row_count - 1))
            {
                GtkTreePath* last_row = gtk_tree_model_get_path(data->sort, &iter);
                pinned_to_new = !gtk_tree_path_compare(last_visible, last_row);
                gtk_tree_path_free(last_row);
            }

            gtk_tree_path_free(last_visible);
        }
    }

    return pinned_to_new;
}

static void scroll_to_bottom(struct MsgData* data)
{
    if (data->sort != NULL)
    {
        GtkTreeIter iter;
        int const row_count = gtk_tree_model_iter_n_children(data->sort, NULL);

        if (gtk_tree_model_iter_nth_child(data->sort, &iter, NULL, row_count - 1))
        {
            GtkTreePath* last_row = gtk_tree_model_get_path(data->sort, &iter);
            gtk_tree_view_scroll_to_cell(data->view, last_row, NULL, TRUE, 1, 0);
            gtk_tree_path_free(last_row);
        }
    }
}

/****
*****
****/

static void level_combo_changed_cb(GtkComboBox* combo_box, gpointer gdata)
{
    struct MsgData* data = gdata;
    int const level = gtr_combo_box_get_active_enum(combo_box);
    gboolean const pinned_to_new = is_pinned_to_new(data);

    tr_logSetLevel(level);
    gtr_core_set_pref_int(data->core, TR_KEY_message_level, level);
    data->maxLevel = level;
    gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(data->filter));

    if (pinned_to_new)
    {
        scroll_to_bottom(data);
    }
}

/* similar to asctime, but is utf8-clean */
static char* gtr_localtime(time_t time)
{
    char buf[256];
    char* eoln;
    struct tm const tm = *localtime(&time);

    g_strlcpy(buf, asctime(&tm), sizeof(buf));

    if ((eoln = strchr(buf, '\n')) != NULL)
    {
        *eoln = '\0';
    }

    return g_locale_to_utf8(buf, -1, NULL, NULL, NULL);
}

static void doSave(GtkWindow* parent, struct MsgData* data, char const* filename)
{
    FILE* fp = fopen(filename, "w+");

    if (fp == NULL)
    {
        GtkWidget* w = gtk_message_dialog_new(parent, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, _("Couldn't save \"%s\""),
            filename);
        gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(w), "%s", g_strerror(errno));
        g_signal_connect_swapped(w, "response", G_CALLBACK(gtk_widget_destroy), w);
        gtk_widget_show(w);
    }
    else
    {
        GtkTreeIter iter;
        GtkTreeModel* model = GTK_TREE_MODEL(data->sort);

        if (gtk_tree_model_iter_children(model, &iter, NULL))
        {
            do
            {
                char* date;
                char const* levelStr;
                struct tr_log_message const* node;

                gtk_tree_model_get(model, &iter, COL_TR_MSG, &node, -1);
                date = gtr_localtime(node->when);

                switch (node->level)
                {
                case TR_LOG_DEBUG:
                    levelStr = "debug";
                    break;

                case TR_LOG_ERROR:
                    levelStr = "error";
                    break;

                default:
                    levelStr = "     ";
                    break;
                }

                fprintf(fp, "%s\t%s\t%s\t%s\n", date, levelStr, node->name != NULL ? node->name : "",
                    node->message != NULL ? node->message : "");
                g_free(date);
            }
            while (gtk_tree_model_iter_next(model, &iter));
        }

        fclose(fp);
    }
}

static void onSaveDialogResponse(GtkWidget* d, int response, gpointer data)
{
    if (response == GTK_RESPONSE_ACCEPT)
    {
        char* file = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(d));
        doSave(GTK_WINDOW(d), data, file);
        g_free(file);
    }

    gtk_widget_destroy(d);
}

static void onSaveRequest(GtkWidget* w, gpointer data)
{
    GtkWindow* window = GTK_WINDOW(gtk_widget_get_toplevel(w));
    GtkWidget* d = gtk_file_chooser_dialog_new(_("Save Log"), window, GTK_FILE_CHOOSER_ACTION_SAVE, GTK_STOCK_CANCEL,
        GTK_RESPONSE_CANCEL, GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL);

    gtk_dialog_set_alternative_button_order(GTK_DIALOG(d), GTK_RESPONSE_ACCEPT, GTK_RESPONSE_CANCEL, -1);
    g_signal_connect(d, "response", G_CALLBACK(onSaveDialogResponse), data);
    gtk_widget_show(d);
}

static void onClearRequest(GtkWidget* w UNUSED, gpointer gdata)
{
    struct MsgData* data = gdata;

    gtk_list_store_clear(data->store);
    tr_logFreeQueue(myHead);
    myHead = myTail = NULL;
}

static void onPauseToggled(GtkToggleToolButton* w, gpointer gdata)
{
    struct MsgData* data = gdata;

    data->isPaused = gtk_toggle_tool_button_get_active(w);
}

static char const* getForegroundColor(int msgLevel)
{
    switch (msgLevel)
    {
    case TR_LOG_DEBUG:
        return "forestgreen";

    case TR_LOG_INFO:
        return "black";

    case TR_LOG_ERROR:
        return "red";

    default:
        g_assert_not_reached();
        return "black";
    }
}

static void renderText(GtkTreeViewColumn* column UNUSED, GtkCellRenderer* renderer, GtkTreeModel* tree_model, GtkTreeIter* iter,
    gpointer gcol)
{
    int const col = GPOINTER_TO_INT(gcol);
    char* str = NULL;
    struct tr_log_message const* node;

    gtk_tree_model_get(tree_model, iter, col, &str, COL_TR_MSG, &node, -1);
    g_object_set(renderer, "text", str, "foreground", getForegroundColor(node->level), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
}

static void renderTime(GtkTreeViewColumn* column UNUSED, GtkCellRenderer* renderer, GtkTreeModel* tree_model, GtkTreeIter* iter,
    gpointer data UNUSED)
{
    struct tm tm;
    char buf[16];
    struct tr_log_message const* node;

    gtk_tree_model_get(tree_model, iter, COL_TR_MSG, &node, -1);
    tm = *localtime(&node->when);
    g_snprintf(buf, sizeof(buf), "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec);
    g_object_set(renderer, "text", buf, "foreground", getForegroundColor(node->level), NULL);
}

static void appendColumn(GtkTreeView* view, int col)
{
    GtkCellRenderer* r;
    GtkTreeViewColumn* c;
    char const* title = NULL;

    switch (col)
    {
    case COL_SEQUENCE:
        title = _("Time");
        break;

    /* noun. column title for a list */
    case COL_NAME:
        title = _("Name");
        break;

    /* noun. column title for a list */
    case COL_MESSAGE:
        title = _("Message");
        break;

    default:
        g_assert_not_reached();
    }

    switch (col)
    {
    case COL_NAME:
        r = gtk_cell_renderer_text_new();
        c = gtk_tree_view_column_new_with_attributes(title, r, NULL);
        gtk_tree_view_column_set_cell_data_func(c, r, renderText, GINT_TO_POINTER(col), NULL);
        gtk_tree_view_column_set_sizing(c, GTK_TREE_VIEW_COLUMN_FIXED);
        gtk_tree_view_column_set_fixed_width(c, 200);
        gtk_tree_view_column_set_resizable(c, TRUE);
        break;

    case COL_MESSAGE:
        r = gtk_cell_renderer_text_new();
        c = gtk_tree_view_column_new_with_attributes(title, r, NULL);
        gtk_tree_view_column_set_cell_data_func(c, r, renderText, GINT_TO_POINTER(col), NULL);
        gtk_tree_view_column_set_sizing(c, GTK_TREE_VIEW_COLUMN_FIXED);
        gtk_tree_view_column_set_fixed_width(c, 500);
        gtk_tree_view_column_set_resizable(c, TRUE);
        break;

    case COL_SEQUENCE:
        r = gtk_cell_renderer_text_new();
        c = gtk_tree_view_column_new_with_attributes(title, r, NULL);
        gtk_tree_view_column_set_cell_data_func(c, r, renderTime, NULL, NULL);
        gtk_tree_view_column_set_resizable(c, TRUE);
        break;

    default:
        g_assert_not_reached();
        break;
    }

    gtk_tree_view_append_column(view, c);
}

static gboolean isRowVisible(GtkTreeModel* model, GtkTreeIter* iter, gpointer gdata)
{
    struct tr_log_message const* node;
    struct MsgData const* data = gdata;

    gtk_tree_model_get(model, iter, COL_TR_MSG, &node, -1);

    return node->level <= data->maxLevel;
}

static void onWindowDestroyed(gpointer gdata, GObject* deadWindow UNUSED)
{
    struct MsgData* data = gdata;

    g_source_remove(data->refresh_tag);

    g_free(data);
}

static tr_log_message* addMessages(GtkListStore* store, struct tr_log_message* head)
{
    tr_log_message* i;
    static unsigned int sequence = 0;
    char const* default_name = g_get_application_name();

    for (i = head; i != NULL && i->next != NULL; i = i->next)
    {
        char const* name = i->name != NULL ? i->name : default_name;

        gtk_list_store_insert_with_values(store, NULL, 0,
            COL_TR_MSG, i,
            COL_NAME, name,
            COL_MESSAGE, i->message,
            COL_SEQUENCE, ++sequence,
            -1);

        /* if it's an error message, dump it to the terminal too */
        if (i->level == TR_LOG_ERROR)
        {
            GString* gstr = g_string_sized_new(512);
            g_string_append_printf(gstr, "%s:%d %s", i->file, i->line, i->message);

            if (i->name != NULL)
            {
                g_string_append_printf(gstr, " (%s)", i->name);
            }

            g_warning("%s", gstr->str);
            g_string_free(gstr, TRUE);
        }
    }

    return i; /* tail */
}

static gboolean onRefresh(gpointer gdata)
{
    struct MsgData* data = gdata;
    gboolean const pinned_to_new = is_pinned_to_new(data);

    if (!data->isPaused)
    {
        tr_log_message* msgs = tr_logGetQueue();

        if (msgs != NULL)
        {
            /* add the new messages and append them to the end of
             * our persistent list */
            tr_log_message* tail = addMessages(data->store, msgs);

            if (myTail != NULL)
            {
                myTail->next = msgs;
            }
            else
            {
                myHead = msgs;
            }

            myTail = tail;
        }

        if (pinned_to_new)
        {
            scroll_to_bottom(data);
        }
    }

    return G_SOURCE_CONTINUE;
}

static GtkWidget* debug_level_combo_new(void)
{
    GtkWidget* w = gtr_combo_box_new_enum(
        _("Error"), TR_LOG_ERROR,
        _("Information"), TR_LOG_INFO,
        _("Debug"), TR_LOG_DEBUG,
        NULL);
    gtr_combo_box_set_active_enum(GTK_COMBO_BOX(w), gtr_pref_int_get(TR_KEY_message_level));
    return w;
}

/**
***  Public Functions
**/

GtkWidget* gtr_message_log_window_new(GtkWindow* parent, TrCore* core)
{
    GtkWidget* win;
    GtkWidget* vbox;
    GtkWidget* toolbar;
    GtkWidget* w;
    GtkWidget* view;
    GtkToolItem* item;
    struct MsgData* data;

    data = g_new0(struct MsgData, 1);
    data->core = core;

    win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_transient_for(GTK_WINDOW(win), parent);
    gtk_window_set_title(GTK_WINDOW(win), _("Message Log"));
    gtk_window_set_default_size(GTK_WINDOW(win), 560, 350);
    gtk_window_set_role(GTK_WINDOW(win), "message-log");
    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);

    /**
    ***  toolbar
    **/

    toolbar = gtk_toolbar_new();
    gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_BOTH_HORIZ);
    gtk_style_context_add_class(gtk_widget_get_style_context(toolbar), GTK_STYLE_CLASS_PRIMARY_TOOLBAR);

    item = gtk_tool_button_new_from_stock(GTK_STOCK_SAVE_AS);
    g_object_set(G_OBJECT(item), "is-important", TRUE, NULL);
    g_signal_connect(item, "clicked", G_CALLBACK(onSaveRequest), data);
    gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);

    item = gtk_tool_button_new_from_stock(GTK_STOCK_CLEAR);
    g_object_set(G_OBJECT(item), "is-important", TRUE, NULL);
    g_signal_connect(item, "clicked", G_CALLBACK(onClearRequest), data);
    gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);

    item = gtk_separator_tool_item_new();
    gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);

    item = gtk_toggle_tool_button_new_from_stock(GTK_STOCK_MEDIA_PAUSE);
    g_object_set(G_OBJECT(item), "is-important", TRUE, NULL);
    g_signal_connect(item, "toggled", G_CALLBACK(onPauseToggled), data);
    gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);

    item = gtk_separator_tool_item_new();
    gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);

    w = gtk_label_new(_("Level"));
    gtk_misc_set_padding(GTK_MISC(w), GUI_PAD, 0);
    item = gtk_tool_item_new();
    gtk_container_add(GTK_CONTAINER(item), w);
    gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);

    w = debug_level_combo_new();
    g_signal_connect(w, "changed", G_CALLBACK(level_combo_changed_cb), data);
    item = gtk_tool_item_new();
    gtk_container_add(GTK_CONTAINER(item), w);
    gtk_toolbar_insert(GTK_TOOLBAR(toolbar), item, -1);

    gtk_box_pack_start(GTK_BOX(vbox), toolbar, FALSE, FALSE, 0);

    /**
    ***  messages
    **/

    data->store = gtk_list_store_new(N_COLUMNS,
        G_TYPE_UINT, /* sequence */
        G_TYPE_POINTER, /* category */
        G_TYPE_POINTER, /* message */
        G_TYPE_POINTER); /* struct tr_log_message */

    addMessages(data->store, myHead);
    onRefresh(data); /* much faster to populate *before* it has listeners */

    data->filter = gtk_tree_model_filter_new(GTK_TREE_MODEL(data->store), NULL);
    data->sort = gtk_tree_model_sort_new_with_model(data->filter);
    g_object_unref(data->filter);
    gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(data->sort), COL_SEQUENCE, GTK_SORT_ASCENDING);
    data->maxLevel = gtr_pref_int_get(TR_KEY_message_level);
    gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(data->filter), isRowVisible, data, NULL);

    view = gtk_tree_view_new_with_model(data->sort);
    g_object_unref(data->sort);
    g_signal_connect(view, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL);
    data->view = GTK_TREE_VIEW(view);
    gtk_tree_view_set_rules_hint(data->view, TRUE);
    appendColumn(data->view, COL_SEQUENCE);
    appendColumn(data->view, COL_NAME);
    appendColumn(data->view, COL_MESSAGE);
    w = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
    gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(w), GTK_SHADOW_IN);
    gtk_container_add(GTK_CONTAINER(w), view);
    gtk_box_pack_start(GTK_BOX(vbox), w, TRUE, TRUE, 0);
    gtk_container_add(GTK_CONTAINER(win), vbox);

    data->refresh_tag = gdk_threads_add_timeout_seconds(SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS, onRefresh, data);
    g_object_weak_ref(G_OBJECT(win), onWindowDestroyed, data);

    scroll_to_bottom(data);
    gtk_widget_show_all(win);
    return win;
}
